{***************************************************************
    SpyGestion  Copyright (C) 2008 Christophe KOALI <nature2gas@gmail.com>
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
    
    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
    
****************************************************************}
unit modNewsGestion;

interface

uses Classes;


////////////////////////////
// Les news sont sauvegardées dans un stream
// Contrairement aux rapports, ici on n'a pas besoin de séparer l'header
// et le contenu des news
// Pourquoi ? car on n'a pas besoin d'afficher séparément le contenu et ses
// informations. Sauf peut-être pour l'historique des 5 dernières news
// mais cela peut se gérer facilement dans le programme sans toucher à
// la structure du fichier
//
// Pour résumer, chaque news sont sauvegardées les unes à la suite des autres
// dans entête globale.
// Toutefois chaque news a sa propre entête. Celle-ci contient:
//  - un id de news pour le programme (utile pour afficher le numéro de la news, et pouvoir modifier le flag deja lu)
//  - l'id de la news dans la BDD (inutile mais puisqu'il est connu autant le sauvegarder)
//  - La date du serveur web lors du téléchargement de la news (inutile mais bon)
//  - La date locale de téléchargement de la news    (pour afficher la date de téléchargement)
//  - La taille du contenu de la news (stream crypté)
//  - La position de la news précédente (la position de l'entête de la news précédente)
//    Comme on lit les news de la plus récente à la plus ancienne, cela évitera de
//    parcourrir tout le stream pour rien
//
// A cela, on ajoute un entête global au fichier, qui contient:
//  - La position de l'entête de la dernière news ajouté
//  - le nombre total de news
//  - Le nombre total de news non lu
//  - La clé IDEA qui a servit à chiffrer toutes les news
//    Edit: on NE chiffre PAS, on les compresse seulement
//    Cela est remplacé par une serie de 32 octets vide
//


const
     CNT_PHP_DATETIME_START = 'txd';
     CNT_PHP_DATETIME_END   = 'gdate';
     CNT_PHP_NEWS_ID        = 'id';
     CNT_PHP_VERSION        = 'y5';

     CNT_NEWS_VERSION  : integer = 3;



type
     TNewsInfos = record
         // id interne du programme  (clé unique)
         // l'id de la base de donnée peut varier entre les mirroirs
         id: integer;

         // id de la base de données
         idbdd : integer;

         date_download: int64;
         date_bdd  : int64;

         taille_contenu: integer;

         dejalu: integer;

         // pour un usage futur certainement
         flags : integer;

         
         // position de la news précédente le stream
         previous_news: integer;

         // de la suivante
         next_news    : integer;
     end;
     PNewsInfos = ^TNewsInfos;

     // pour les valeurs booleannes
     TNewsArrayByte  = array[0..50] of byte;

     TNewsArrayInt   = array[0..31] of integer;
     TNewsArrayInt64 = array[0..15] of int64;

     TNewsHeader = record
         FileVersion     : integer;
         PositionLastNews: integer;
         TotalNews       : integer;
         TotalNewsNonLu  : integer;

         ConfInteger    : TNewsArrayInt;
         ConfInt64      : TNewsArrayInt64;
         ConfBool       : TNewsArrayByte;
     end;

     TNewsCollection = class(TObject)
     private
         // fichier news
         FStream: TFileStream;

         FHeader  : TNewsHeader;


         FErreurDurantLajout: boolean;


         // position de l'entete de la news actuellement lu
         // la position a change car on a lu le contenu de celle-ci
         // on la mémorise ici pour pouvoir changer le flag dejalu si nécessaire
         FCurrentNewsReadingPosition: integer;

         FCurrentNewsReadingInfos   : TNewsInfos;

         // cette date doit être accessible à l'extérieur car celle ci
         // sera utilisé lors de la prochaine mise à jour
         FDateWebDL: int64;

         FFichier: string;

         FNextIdToUse: integer;

         function AddNews(Content: string; IdBDD: integer; DateBDD: int64): boolean;


         function FinalizeLoadByAnalyseLastNews: boolean;
         procedure InitHeader;

         procedure sGoTo(Position: int64); inline;

     public
         function IsValidStream: boolean;

         property TotalNews : integer  read FHeader.TotalNews;
         property TotalNonLu: integer  read FHeader.TotalNewsNonLu;

         property ErreurDurantAjout : boolean read FErreurDurantLajout;

         property LastPageDLTime: int64 read FDateWebDL write FDateWebDL;


         // place les infos dans infos (met id à -1 si news invalide)
         // et la fonction renvoi le contenu de la news en clair
         function LoadCurrentNews(Infos: PNewsInfos): string;

         function LoadCurrHeader: boolean;



         // renvoi True si la news existe (sinon False et alors il n'y a aucune news)
         function GoToLastNews: boolean;

         // renvoi True si il existe une news précédente (plus ancienne donc)
         // et s'y déplace
         // sinon False
         function GoToPreviousNews: boolean;

         // renvoi True si il existe une news suivante (plus récente donc)
         // et s'y déplace
         // sinon False
         function GoToNextNews: boolean;


         // renvoi True si il existe une news, et se déplace à la toute première
         // ajouté
         // sinon False
         function GoToFirstNews: boolean;




         // la date est en début et fin de données
         // c'est à partir d'elle qu'on détermine si l'on a bien reçu les news
         function ParsingNews(Content: string): integer;


         // Charge uniquement la position de la dernière news
         // ainsi que le stream
         function LoadFromFile(Fichier: string): boolean;

         // il n'y a pas de précedure SaveToFile car les news sont directement
         // écrite dans le fichier dès leur parsing
         
         procedure CloseStream;

         constructor Create;
         destructor Destroy; override;

     end;


implementation

uses ZLibEx,
     ESBDates,
     Math,
     SysUtils,
     modInteractionAuteur,
     modInteractionKey,
     sFonctionsChaines;
     

function TNewsCollection.GoToFirstNews: boolean;
begin

    if (FStream = nil) or (FHeader.TotalNews < 1) then begin
       Result := false;
       exit;
    end;

    Result := true;
    sGoto(SizeOf(FHeader));

end;

// renvoi True si il existe une news précédente (plus ancienne donc)
// et s'y déplace
// sinon False
function TNewsCollection.GoToPreviousNews: boolean;
begin
     if FCurrentNewsReadingInfos.id < 1 then begin
        // news invalide
        Result := false;
        exit;
     end;

     if FCurrentNewsReadingInfos.previous_news < SizeOf(TNewsHeader) then begin
        // plus de news
        Result := false;
        exit;
     end;

     sGoto(FCurrentNewsReadingInfos.previous_news);
     Result := true;
end;

// renvoi True si il existe une news suivante (plus récente donc)
// et s'y déplace
// sinon False
function TNewsCollection.GoToNextNews: boolean;
begin
     if FCurrentNewsReadingInfos.id < 1 then begin
        // news invalide
        Result := false;
        exit;
     end;

     if FCurrentNewsReadingInfos.next_news >= FStream.Size then begin
        // plus de news
        Result := false;
        exit;
     end;

     sGoto(FCurrentNewsReadingInfos.next_news);
     Result := true;
end;



function TNewsCollection.GoToLastNews: boolean;
begin
    if (FStream = nil) or (FHeader.TotalNews < 1) then begin
       Result := false;
       exit;
    end;


    sGoto(FHeader.PositionLastNews);
    Result := true;
end;

function TNewsCollection.IsValidStream: boolean;
begin
    Result := FStream <> nil;
end;

function TNewsCollection.ParsingNews(Content: string): integer;
const CNT_DATE_INIT : int64 = 0;
var Ligne: string;
    CurrServerTimeStart, CurrServerTimeEnd: int64;

    // en cas d'erreur, d'ajout de news...
    BackupPosition: int64;

    xPos: integer;

    NewsID: integer;
    bInNewsData: boolean;

    CryptedNewsData, NewsNormal: string;
begin

(*
     Structure:
      echo "txd=" . $curr_time;

		echo "id=" . $row['id'] . "\n";            (boucle pour chaque news)
		echo "-------- BEGIN ------\n";
		echo $row['contenu'];
		echo "-------- END ------\n\n";

      echo "\ngdate=" . $curr_time . "\n\n";
*)

    Result := 0;
    FErreurDurantLajout := false;


    Content := sfcRemplace(Content, #$0D, '');
    bInNewsData := false;
    NewsID      := -1;
    CryptedNewsData := '';
    CurrServerTimeStart := CNT_DATE_INIT;
    CurrServerTimeEnd   := CNT_DATE_INIT;

    FStream.Seek(0, soEnd);

    while (Content <> '') do begin
         xPos := pos(#$0A, Content);
         if xPos = 0 then begin
            Ligne := Content;
            Content := '';
         end else begin
            Ligne := copy(Content, 1, xPos - 1);
            System.Delete(Content, 1, xPos);
         end;

         Ligne := trim(Ligne);

         if bInNewsData then begin
             if pos('-- end --', lowercase(Ligne)) > 0 then begin
                bInNewsData := false;

                // on déchiffre puis on ajoute

                NewsNormal := iaRSADecryptageKeyStream(CryptedNewsData, RSA_GetPublicKey, CNT_PUBLICKEY_PASSPHRASE);

                if length(NewsNormal) > 10 then begin
                   // le stream a bien été déchiffré
                   // c'est un fichier de news valide
                   BackupPosition := FStream.Position;

                   if AddNews(NewsNormal, NewsID, CurrServerTimeStart) then begin
                      Inc(Result);
                   end else begin
                      FStream.Seek(BackupPosition, soBeginning);
                      FErreurDurantLajout := true;
                   end;

                end else begin
                   // erreur de déchiffrage
                   // on stop de parsing
                   if Result = 0 then begin
                      // aucune news n'a déjà été ajouté
                      // c'est parfait
                      Result := -1;
                      exit;
                   end;
                end;

                NewsNormal := '';
                CryptedNewsData := '';
                NewsID := 0;

             end else begin
                // ajoute les données
                CryptedNewsData := CryptedNewsData + Ligne;
             end;

         end else begin
             Ligne := lowercase(Ligne);

             if pos('-- begin --', Ligne) > 0 then begin
                if (NewsID >= 0) and (CurrServerTimeStart > 100) and (CurrServerTimeEnd = CNT_DATE_INIT) then begin
                   bInNewsData := true;
                   CryptedNewsData := '';
                end;

             end else begin
                xPos := pos('=', Ligne);

                if xPos > 0 then begin
                   NewsNormal := copy(Ligne, 1, xPos - 1);
                   System.Delete(Ligne, 1, xPos);
                   Ligne := trim(Ligne);

                   if SameText(NewsNormal, CNT_PHP_DATETIME_START) then begin
                       try
                           CurrServerTimeStart := StrToInt(Ligne);
                       except
                           CurrServerTimeStart := CNT_DATE_INIT;
                       end;

                   end else if SameText(NewsNormal, CNT_PHP_DATETIME_END) then begin
                       try
                           CurrServerTimeEnd := StrToInt(Ligne);
                       except
                           CurrServerTimeEnd := CNT_DATE_INIT;
                       end;


                   end else if SameText(NewsNormal, CNT_PHP_NEWS_ID) then begin
                       try
                           NewsID := StrToIntDef(Ligne, -1);
                       except
                           NewsID := -1;
                       end;

                   end else if SameText(NewsNormal, CNT_PHP_VERSION) then begin
                       // pour un usage futur


                   end;

                end;

             end;
         end;

         
    end;


    if CurrServerTimeStart = CurrServerTimeEnd then begin
       // même date
       // la page web est valide
       FDateWebDL := CurrServerTimeStart;

    end else begin
       // date différente
       if Result = 0 then begin
          Result := -1;
          exit;
       end;

       FDateWebDL := max(CurrServerTimeStart, CurrServerTimeEnd);

    end;

    if FDateWebDL < 10000 then
       FDateWebDL := 0;


    // Met à jour l'header du fichier
    sGoTo(0);
    FStream.Write(FHeader, SizeOf(FHeader));
    FStream.Seek(0, soEnd);

end;

function TNewsCollection.AddNews(Content: string; IdBDD: integer; DateBDD: int64): boolean;
var iNews: TNewsInfos;
    CompressionStream: TZCompressionStream;
    InputStream: TMemoryStream;
    bError: boolean;
    sPositionBackup: integer;
begin

    Result := false;
    if Content = ''  then exit;
    if FStream = nil then exit;



    // s'assure d'être à la fin du fichier
    // Edit: cela DOIT être fait dans la procédure appellante
    //FStream.Seek(0, soEnd);


    iNews.id := FNextIdToUse;
    Inc(FNextIdToUse);

    iNews.idbdd := IdBDD;
    iNews.date_download := DateTimeToUnix(Now);
    iNews.date_bdd := dateBDD;

    iNews.taille_contenu := length(Content);
    iNews.dejalu := 0;
    iNews.flags := 0;
    iNews.previous_news := FHeader.PositionLastNews;
    iNews.next_news     := 0;

    sPositionBackup := FStream.Position;



    // ecrit l'entete de la news:
    if FStream.Write(iNews, SizeOf(iNews)) < SizeOf(iNews) then
       exit;


    // ecrit la news
    InputStream := TMemoryStream.Create;
    InputStream.Write(Content[1], length(Content));
    InputStream.Seek(0, soBeginning);


    bError := false;
    try
       CompressionStream := TZCompressionStream.Create(FStream, zcMax);
       CompressionStream.CopyFrom(InputStream, InputStream.Size);
       CompressionStream.Free;
    except
       bError := true;
    end;
    InputStream.Free;

    if bError then
       exit;


    iNews.next_news := FStream.Size;

    // met à jour l'entete de la news:
    sGoto(sPositionBackup);
    if FStream.Write(iNews, SizeOf(iNews)) < SizeOf(iNews) then
       exit;

    // retourne à la fin du fichier
    FStream.Seek(0, soEnd);
    

    // met à jour l'header du fichier
    FHeader.PositionLastNews := sPositionBackup;
    FHeader.TotalNews        := FHeader.TotalNews + 1;
    FHeader.TotalNewsNonLu   := FHeader.TotalNewsNonLu + 1;

    // mais ne la sauvegarde pas encore
    // la sauvegarde est effectuée une fois que toutes les news sont ajoutées

    Result := true;
end;

procedure TNewsCollection.InitHeader;
begin

    // initialise tout à 0
    FillChar(FHeader, SizeOf(FHeader), 0);

    // sauf le fileversion
    FHeader.FileVersion := CNT_NEWS_VERSION;


    FNextIdToUse                := 1;
    FCurrentNewsReadingPosition := 0;

    FDateWebDL := 0;

end;

function TNewsCollection.LoadCurrHeader: boolean;
var backPos: integer;
    Infos: TNewsInfos;
begin

    backPos := FStream.Position;

    if FStream.Read(Infos, SizeOf(TNewsInfos)) < SizeOf(TNewsInfos) then begin
       // erreur
       Result := false;
       exit;
    end;


    // duplique les données lues
    Move(Infos, FCurrentNewsReadingInfos, SizeOf(FCurrentNewsReadingInfos));
    Result := true;

    // retourne à la position d'origine
    sGoto(backPos);
end;


function TNewsCollection.LoadCurrentNews(Infos: PNewsInfos): string;
var DeCompressionStream: TZDecompressionStream;
    OutputStream: TMemoryStream;
    bError: boolean;
    bUpdateHeader: boolean;
    sBackupPosition: int64;
begin
    Result := '';

    if FStream = nil then begin
       if Infos <> nil then
          Infos.id := -1;

       exit;
    end;

    FCurrentNewsReadingPosition := FStream.Position;

    if FStream.Read(Infos^, SizeOf(TNewsInfos)) < SizeOf(TNewsInfos) then begin
       Infos.id := -1;
       exit;
    end;


    // duplique les données lues
    Move(Infos^, FCurrentNewsReadingInfos, SizeOf(FCurrentNewsReadingInfos));



    // on est à présent sur le stream compressé de la news
    if Infos.taille_contenu <= 0 then begin
       // pas de contenu...
       Infos.id := -1;
       exit;
    end;

    bUpdateHeader := false;

    if FCurrentNewsReadingInfos.dejalu = 0 then begin
       FCurrentNewsReadingInfos.dejalu := 1;
       FHeader.TotalNewsNonLu := FHeader.TotalNewsNonLu - 1;
       if FHeader.TotalNewsNonLu < 0 then
          FHeader.TotalNewsNonLu := 0;

       bUpdateHeader := true;


       // met à jour les informations de la news
       sGoTo(FCurrentNewsReadingPosition);
       FStream.Write(FCurrentNewsReadingInfos, SizeOf(TNewsInfos));

       // en écrivant, le stream s'est bien repositionné là où il fallait
    end;

    OutputStream := TMemoryStream.Create;

    bError := false;
    try
       DecompressionStream := TZDecompressionStream.Create(FStream);
       OutputStream.CopyFrom(DecompressionStream, Infos.taille_contenu);
       DecompressionStream.Free;
    except
       bError := true;
    end;

    if bError then begin
       Infos.id := -1;
       OutputStream.Free;
       exit;
    end;

    OutputStream.Seek(0, soBeginning);

    SetLength(Result, OutputStream.Size);
    OutputStream.Read(Result[1], OutputStream.Size);

    OutputStream.Free;

    if bUpdateHeader then begin
       sBackupPosition := FStream.Position;
       sGoTo(0);
       FStream.Write(FHeader, SizeOf(FHeader));
    end;

    sGoto(sBackupPosition);
end;


procedure TNewsCollection.sGoTo(Position: int64);
begin
    FStream.Seek(Position, soBeginning);
end;

function TNewsCollection.FinalizeLoadByAnalyseLastNews: boolean;
var nHeader: TNewsInfos;
begin

    if FHeader.FileVersion <> CNT_NEWS_VERSION then begin
       // Erreur...
       Result := False;
       exit;
    end;


    Result := true;

    if FHeader.PositionLastNews < SizeOf(TNewsHeader) then
       // aucune news, resultat à True
       exit;


    // essaye de lire la dernière news
    sGoTo(FHeader.PositionLastNews);

    if FStream.Read(nHeader, SizeOf(nHeader)) < SizeOf(nHeader) then begin
       // erreur
       // base de données corrompu ?
       Result := false;

    end else begin
       // lecture réussi
       // détermine le prochain ID à utiliser
       FNextIdToUse := nHeader.id + 1;
    end;


end;

function TNewsCollection.LoadFromFile(Fichier: string): boolean;
begin

    Result := false;

    InitHeader;


    // A priori inutile puisque le stream n'est jamais fermé
    FFichier := Fichier;

    if FileExists(Fichier) = false then begin
       // le fichier n'existe pas...
       // on le cree
       try
          FStream := TFileStream.Create(Fichier, fmCreate);
          FStream.Write(FHeader, SizeOf(TNewsHeader));
       finally
          FStream.Free;
       end;
       
    end;

    
    try
       FStream := TFileStream.Create(Fichier, fmOpenReadWrite);
    except
       // EFOpenError
       // le fichier doit être utilisé par un autre processus
       // (une autre instance du robot probablement)
       FStream := nil;
    end;

    if FStream = nil then begin
       exit;
    end;

    Result := true;

    
    if FStream.Size < SizeOf(TNewsHeader) then begin
       // écrit l'entête
       if FStream.Write(FHeader, SizeOf(TNewsHeader)) < SizeOf(TNewsHeader) then begin
          // erreur d'écriture (disque dur plein ?)
          Result := false;
          FStream.Free;
          FStream := nil;
       end;
       exit;
    end;

    if FStream.Read(FHeader, SizeOf(TNewsHeader)) < SizeOf(TNewsHeader) then begin
       // erreur
       exit;
    end;


    Result := FinalizeLoadByAnalyseLastNews;

    if Result then begin
       // se positionne à la fin
       FStream.Seek(0, soEnd);

    end else begin
       // sinon erreur...
       // on ferme le Stream
       FStream.Free;
       FStream := nil;
    end;


end;

procedure TNewsCollection.CloseStream;
begin
     if FStream <> nil then begin
        FStream.Seek(0, soEnd);
        FStream.Free;
        FStream := nil;
     end;

end;


constructor TNewsCollection.Create;
begin
    inherited Create;

    FStream           := nil;
    InitHeader;
end;


destructor TNewsCollection.Destroy;
begin
     CloseStream;

     inherited Destroy;
end;




end.
